Skip to main content

Models structure

Structuring

Problem

We have paginated list model

export const exercisesListRequestFx = createEffect(({ page = 1 } = {}) => {
return fetch(`https://api.example.com/exercises-list?page=${page}`)
})

export const $requestSettings = createStore(null)
.on(exercisesListRequestFx.done, (_, {params, result}) => ({
page: params.page + 1,
totalPages: result.totalPages,
}))

export const $exerciseList = createStore([]).on(exercisesListRequestFx.done, (state, {params, result}) => {
if(params.page === 1) return result.data
return [...state, ...result.data]
})

export const pageEndReached = createEvent()

sample({
source: $requestSettings,
clock: pageEndReached,
filter: ({page, totalPages}) => page < totalPages,
fn: ({ page }) => ({ page }),
target: fetchExerciseList
})



const Component = () => {
const list = useUnit($exerciseList)

useEffect(() => {
exercisesListRequestFx()
}, [])

return (
<List data={list} onEndReach={() => pageEndReached()}/>
)

}

Encapsulation of models

The first issue with our current approach is that we have methods like pageEndReached that can be easily confused with other similar methods. To avoid this, we should use more specific names like exercisesPageEndReached.

Another problem is our use of $requestSettings. Right now, we're using it internally, but what if we want to use it elsewhere? We would need to export it and to keep things clear, we might have to rename it to something like $exercisesRequestSettings.

It becomes confusing really quickly, this approach forces us to prefix our code a lot.

A better solution would be to encapsulate all of this related logic into a single model. This approach would improve organisation and clarity in our code without the need for prefixing:

export class exercisesModel {
public static getPage = createEffect(({ page = 1 } = {}) => {
return fetch(`https://api.example.com/exercises-list?page=${page}`)
})


public static $requestSettings = createStore(null)
.on(this.getPage.done, (_, {params, result}) => ({
page: params.page + 1,
totalPages: result.totalPages,
}))

public static $list = createStore([]).on(this.getPage.done, (state, {params, result}) => {
//params is page number
if(params.page === 1) return result.data
return [...state, ...result.data]
})

public static pageEndReached = createEvent()
}

sample({
source: exercisesModel.$requestSettings,
clock: exercisesModel.pageEndReached,
filter: ({page, totalPages}) => page < totalPages,
fn: ({ page }) => ({ page }),
target: exercisesModel.getPage
})


So in this case you have class that represents public API of our model. (And you don't need to know how every method named in a model)

Reusing of models

Paginated list is quite common thing. So if we go further we can do a reusable model


type PaginatedGetter = (props: {page: number} | void) => Promise<{ data: T, totalPages: number }>

export class PaginatedListModel<T> {
private getPage: Effect<{page: number}, T[]>

private $requestSettings = createStore(null)
.on(this.getPage.done, (_, {params, result}) => ({
page: params.page + 1,
totalPages: result.totalPages,
}))

public $list = createStore<T[]>([]).on(this.getPage.done, (state, {params, result}) => {
if(params.page === 1) return result.data
return [...state, ...result.data]
})

public pageEndReached = createEvent()

constructor(pageGetter: (props: {page: number} | void) => Promise<T>){
this.getPage = createEffect(pageGetter)

sample({
source: ExercisesModel.$requestSettings,
clock: ExercisesModel.pageEndReached,
filter: ({page, totalPages}) => page < totalPages,
fn: ({ page }) => ({ page }),
target: this.getPage
})
}

public getFirstPage(){
this.getPage({ page: 1 })
}
}

Usage


const exercisesListRequest = ({page}) => {
return fetch(`https://api.example.com/exercises-list?page=${page}`)
}

const exercisesModel = new PaginatedListModel(exercisesListRequest)

What if we need to pass not only page to a request


const exercisesListRequest = attach({
source: {
preferences: $userPreferences,
userData: $userData,
},
mapParams: ({page = 1}, { preferences, userData }) => ({
preferences,
teacherId: userData.teacherId,
userStatus: userData.status,
page
}),
effect: createEffect(({preferences, teacherId, userStatus, page = 1}) => {
return fetch(`https://api.example.com/exercises-list?prefs=${preferences}
&teacherId=${teacherId}
&status=${userStatus}
&page=${page}`
)
})
})

const exercisesModel = new PaginatedListModel(exercisesListRequest)

One step further, and we can create component that works with our model


type PaginatedListProps<T> = {
model: PaginatedListModel<T>
}

const PaginatedList = ({model}) => {

const list = useUnit(model.$list)

useEffect(() => {
model.getFirstPage()
}, [model])

return (
<List data={list} onEndReach={() => model.pageEndReached()}/>
)
}

so back to our exercises context we can get


const exercisesListModel = new PaginatedListModel(exercisesListRequest)

const ExercisesList = () => {
return (
<PaginatedList model={exercisesListModel}/>
)
}

Avoiding circular dependencies

Circular deps happens when you have one model (file) depend on another model (file) and vice versa.

❌With circular:

//exercises.ts
import { $books } from './books'

const getExercises = attach({
source: $books,
mapParams: ({page = 1}, books) => ({books, page}),
effect: createEffect(({books, page = 1}) => {
return fetch(`https://api.example.com/exercises-list?books=${books}&page=${page}`)
})
})

//books.ts
import { getExercises } from './books'

const $books = createStore([])

$books.watch(() => {
getExercises({page: 1})
})

✅Refactor this:

//exercises.ts

const getExercises = createEffect(({books, page}) => {
return fetch(`https://api.example.com/exercises-list?books=${books}&page=${page}`)
})

//books.ts
import { getExercises } from './books'

const $books = createStore([])

$books.watch((books) => {
getExercises({page: 1, books})
})

✅Or this way:

//exercises.ts
const getExercises = createEffect(({books, page}) => {
return fetch(`https://api.example.com/exercises-list?books=${books}&page=${page}`)
})

//books.ts
import { getExercises } from './books'

const $selectedBooks = createStore([])

const getExercisesFromSelectedBooks = attach({
source: $selectedBooks,
mapParams: ({page = 1}, books) => ({books, page}),
effect: createEffect(({books, page = 1}) => {
return fetch(`https://api.example.com/exercises-list?books=${books}&page=${page}`)
})
})

❌With circular:

//logout.ts
const logout = createEvent()

logout.watch(() => {
resetBooks()
resetExercises()
resetSettings()
})

//settings.ts
const resetSettings = createEvent()

const $settings = createStore()
const wrongSettingsSet = createEvent()

sample({
source: $settings,
clock: wrongSettingsSet,
filter: (settings) => settings === null,
target: logout
})